來重現 MUI 的按鈕(包含波紋漣漪效果)吧。
不過因為實作動畫效果的原始碼比預期的長,故實心、外框與純文字按鈕的 CSS 實作內容放到明天來示範。
const defaultButtonStyle = css({
position: 'relative',
overflow: 'hidden',
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
});
透過 position: relative
與 overflow: hidden
來把漣漪動畫限制在「按鈕」的範圍內。
置中透過 display: inline-flex
來處理。
而在按鈕呈現 disable
狀態時,透過 pointer-events: none
來讓按鈕忽略點擊事件。
點擊按鈕後,將一個圓形且逐漸擴大並變為透明的元件掛載到按鈕上,這個元件就是漣漪動畫效果的本體。
相關程式碼與解說如下:
// 動畫效果:設定 scale 讓漣漪放大,並同時透明度變為 0
const rippleAnimation = keyframes`
to {
transform: scale(1.2);
opacity: 0;
}
`;
// 設定按鈕為 overflow: hidden 把漣漪的範圍限制在按鈕之內
const defaultButtonStyle = css({
position: 'relative',
overflow: 'hidden',
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
});
// 預設漣漪樣式,允許使用者透過 props.rippleColor 來直接控制漣漪顏色
const rippleStyle = useMemo(
() =>
css({
position: 'absolute',
borderRadius: '50%',
backgroundColor: rippleColor,
transform: 'scale(0)',
animation: `${rippleAnimation} .7s ease`,
}),
[rippleColor]
);
const playRipple = useCallback(
(e: MouseEvent): void => {
// props.disableRipple 時,不執行任何關於漣漪動畫的計算
if (disableRipple) return;
// 根據點擊目標(也就是我們的按鈕元件)來計算漣漪的直徑,採用的是「按鈕元件長與寬比較大」的那一個數字
const target = e.currentTarget as HTMLButtonElement;
const diameter = Math.max(target.clientWidth, target.clientHeight);
const radius = diameter / 2;
// 透過 rippleContainerRef.current 來操作「負責裝載漣漪動畫的 span 元件」
const rippleContainer = rippleContainerRef.current;
if (rippleContainer) {
const rippleEffect = document.createElement('span');
// 設定漣漪的尺寸與漣漪動畫開始的位置
rippleEffect.style.width = rippleEffect.style.height = `${diameter}px`;
rippleEffect.style.left = `${e.clientX - (target.offsetLeft + radius)}px`;
rippleEffect.style.top = `${e.clientY - (target.offsetTop + radius)}px`;
// 加上漣漪的動畫樣式 rippleStyle
rippleEffect.classList.add(rippleStyle);
// 把漣漪 span 元件掛載到畫面上,而根據 rippleStyle 的設定,漣漪會在 0.7 秒後變為完全透明
// 注意:動畫結束後,漣漪 span 元件還停留在 DOM 上,而我們需要在動畫結束後移除這個元件,否則多次點擊後會新增許多不必要的 span 元件
rippleContainer.appendChild(rippleEffect);
}
},
[disableRipple, rippleContainerRef, rippleStyle]
);
const removeRipple = useCallback((): void => {
const rippleContainer = rippleContainerRef.current;
if (rippleContainer) {
rippleContainer.childNodes.forEach((node) => {
// 檢查「負責裝載漣漪動畫的 span 元件」,若其中有 ELEMENT_NODE 且 class 包含 rippleStyle 的話,移除該「漣漪 span 元件」
if (node.nodeType === 1) {
const elementNode = node as HTMLElement;
if (elementNode.classList.contains(rippleStyle)) {
elementNode.remove();
}
}
});
}
}, [rippleContainerRef, rippleStyle]);
useEffect(() => {
const button = buttonRef.current;
button?.addEventListener('click', playRipple);
// 漣漪動畫結束時,執行 removeRipple 來移除漣漪 span 元件
button?.addEventListener('animationend', removeRipple);
return () => {
button?.removeEventListener('click', playRipple);
button?.removeEventListener('animationend', removeRipple);
};
}, [buttonRef, playRipple, removeRipple]);
rippleStyle
中 animation
的時間長短。rippleAnimation
的 scale()
數字。漣漪效果沒有想像中容易,動手寫了才知道複雜。沒事不需要自幹,吃力不討好。
但不做動畫效果的話倒是蠻簡單的 (゚ ∀ ゚)